其他
1.6 万字长文带你读懂 Java IO
来源 | Java建设者(ID: javajianshe)
BIO NIO 和 AIO 的区别
什么是流
顺序读写。读写数据时,大部分情况下都是按照顺序读写,读取时从文件开头的第一个字节到最后一个字节,写出时也是也如此(RandomAccessFile 可以实现随机读写);
字节数组。读写数据时本质上都是对字节数组做读取和写出操作,即使是字符流,也是在字节流基础上转化为一个个字符,所以字节数组是 IO 流读写数据的本质。
流的分类
输入流:从磁盘或者其它设备中将数据输入到进程中; 输出流:将进程中的数据输出到磁盘或其它设备上保存。
字节流:以字节(8 bit)为单位做数据的传输 字符流:以字符为单位(1字符 = 2字节)做数据的传输
输入流:可以将字节流 => 字符流 输出流:可以将字符流 => 字节流
节点流和处理流
节点流:节点流是真正传输数据的流对象,用于向特定的一个地方(节点)读写数据,称为节点流。例如 FileInputStream 处理流:处理流是对节点流的封装,使用外层的处理流读写数据,本质上是利用节点流的功能,外层的处理流可以提供额外的功能。处理流的基类都是以 Filter 开头。
Java IO 的核心类 File
访问文件的属性:绝对路径、相对路径、文件名······ 文件检测:是否文件、是否目录、文件是否存在、文件的读/写/执行权限······ 操作文件:创建目录、创建文件、删除文件······
r(Read):代表该文件可以被当前用户读,操作权限的序号是 4 w(Write):代表该文件可以被当前用户写,操作权限的序号是 2 x(Execute):该文件可以被当前用户执行,操作权限的序号是 1
文件所有者:拥有的权限是红框中的前三个字母,-代表没有某个权限; 文件所在组的所有用户:拥有的权限是红框中的中间三个字母; 其它组的所有用户:拥有的权限是红框中的最后三个字母;
Java IO 流对象
根据数据流向分为输入流和输出流; 根据数据类型分为字节流和字符流。
字节流对象
InputStream
在读入字节的过程中可以将读取到的字节数据回退给缓冲区中保存,下次可以再次从缓冲区中读出该字节数据。所以PushBackInputStream 允许多次读取输入流的字节数据,只要将读到的字节放回缓冲区即可。
第一次读取缓冲区的数据,判断该数据由哪些线程读取 回退数据,唤醒对应的线程读取数据 重复前两步 关闭输入流
ByteArrayInputStream 和 FileInputStream 是两种基本的节点流,他们分别从字节数组 和 本地文件中读取数据。 DataInputStream、BufferedInputStream 和 PushBackInputStream 都是处理流,对基本的节点流进行封装并增强。 PipiedInputStream 用于多线程通信,可以与其它线程公用一个管道,读取管道中的数据。 ObjectInputStream 用于对象的反序列化,将对象的字节数据读入内存中,通过该流对象可以将字节数据转换成对应的对象。
OutputStream
OutputStream 是所有输出字节流的抽象基类。 ByteArrayOutputStream 和 FileOutputStream 是两种基本的节点流,它们分别向字节数组和本地文件写出数据。 DataOutputStream、BufferedOutputStream 是处理流,前者可以将字节数据转换成基本数据类型写出到文件中;后者是缓冲字节数组,只有在缓冲区满时,才会将所有的字节写出到目的地,减少了 IO 次数。 PipedOutputStream 用于多线程通信,可以和其它线程共用一个管道,向管道中写入数据。 ObjectOutputStream 用于对象的序列化,将对象转换成字节数组后,将所有的字节都写入到指定位置中。 PrintStream 在 OutputStream 基础之上提供了增强的功能,即可以方便地输出各种类型的数据(而不仅限于byte型)的格式化表示形式,且 PrintStream 的方法从不抛出 IOEception,其原理是写出时将各个数据类型的数据统一转换为 String 类型,我会在讲解完。
字符流对象
Reader
Reader 是所有字符输入流的抽象基类; CharArrayReader 和 StringReader 是两种基本的节点流,它们分别从读取 字符数组 和 字符串 数据,StringReader 内部是一个 String 变量值,通过遍历该变量的字符,实现读取字符串,本质上也是在读取字符数组; PipedReader 用于多线程中的通信,从共用地管道中读取字符数据; BufferedReader 是字符输入缓冲流,将读入的数据放入字符缓冲区中,实现高效地读取字符; InputStreamReader 是一种转换流,可以实现从字节流转换为字符流,将字节数据转换为字符;
Writer
Writer 是所有的输出字符流的抽象基类; CharArrayWriter、StringWriter 是两种基本的节点流,它们分别向Char 数组、字符串中写入数据。StringWriter 内部保存了 StringBuffer 对象,可以实现字符串的动态增长; PipedWriter 可以向共用的管道中写入字符数据,给其它线程读取。 BufferedWriter 是缓冲输出流,可以将写出的数据缓存起来,缓冲区满时再调用 flush() 写出数据,减少 IO 次数。 PrintWriter 和 PrintStream 类似,功能和使用也非常相似,只是写出的数据是字符而不是字节。 OutputStreamWriter 将字符流转换为字节流,将字符写出到指定位置;
字节流与字符流的转换
InputStreamReader:从字节流转换为字符流,将字节数据转换为字符数据读入到内存;
OutputStreamWriter:从字符流转换为字节流,将字符数据转换为字节数据写出到指定位置。
BIO 如果遇到 IO 阻塞时,线程将会被挂起,直到 IO 完成后才唤醒线程,线程切换带来了额外的开销。
BIO 中每个 IO 都需要有对应的一个线程去专门处理该次 IO 请求,会让服务器的压力迅速提高。
新潮的 NIO
缓冲区(Buffer)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
put():将数据写入到缓冲区中 get():从缓冲区中读取数据
capacity:缓冲区中最大存储数据的容量,一旦声明则无法改变 limit:表示缓冲区中可以操作数据的大小,limit 之后的数据无法进行读写。必须满足 limit <= capacity position:当前缓冲区中正在操作数据的下标位置,必须满足 position <= limit mark:标记位置,调用 reset() 将 position 位置调整到 mark 属性指向的下标位置,实现多次读取数据
flip():可以实现读写模式的切换,我们可以看看里面的源码
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
rewind():可以将 position 位置设置为 0,再次读取缓冲区中的数据; clear():清空整个缓冲区,它会将 position 设置为 0,limit 设置为 capacity,可以写整个缓冲区;
public Class Main {
public static void main(String[] args) {
// 分配内存大小为11的整型缓存区
IntBuffer buffer = IntBuffer.allocate(11);
// 往buffer里写入2个整型数据
for (int i = 0; i < 2; ++i) {
int randomNum = new SecureRandom().nextInt();
buffer.put(randomNum);
}
// 将Buffer从写模式切换到读模式
buffer.flip();
System.out.println("position >> " + buffer.position()
+ "limit >> " + buffer.limit()
+ "capacity >> " + buffer.capacity());
// 读取buffer里的数据
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
System.out.println("position >> " + buffer.position()
+ "limit >> " + buffer.limit()
+ "capacity >> " + buffer.capacity());
}
}
执行结果如下图所示,首先我们往缓冲区中写入 2 个数据,position 在写模式下指向下标 2,然后调用 flip() 方法切换为读模式,limit 指向下标 2,position 从 0 开始读数据,读到下标为 2 时发现到达 limit 位置,不可继续读。
通道(Channel)
文件 IO:FileInputStream、FileOutputStream、RandomAccessFile TCP 网络 IO:Socket、ServerSocket UDP 网络 IO:DatagramSocket
示例:文件拷贝案例。
打开原文件的输入流通道,将字节数据读入到缓冲区中 打开目的文件的输出流通道,将缓冲区中的数据写到目的地 关闭所有流和通道(重要!)
public class Test {
/** 缓冲区的大小 */
public static final int SIZE = 1024;
public static void main(String[] args) throws IOException {
// 打开文件输入流
FileChannel inChannel = new FileInputStream("d:\小菠萝\小菠萝.jpg").getChannel();
// 打开文件输出流
FileChannel outChannel = new FileOutputStream("d:\小菠萝分身\小菠萝-拷贝.jpg").getChannel();
// 分配 1024 个字节大小的缓冲区
ByteBuffer dsts = ByteBuffer.allocate(SIZE);
// 将数据从通道读入缓冲区
while (inChannel.read(dsts) != -1) {
// 切换缓冲区的读写模式
dsts.flip();
// 将缓冲区的数据通过通道写到目的地
outChannel.write(dsts);
// 清空缓冲区,准备下一次读
dsts.clear();
}
inChannel.close();
outChannel.close();
}
}
BIO 和 NIO 拷贝文件的区别
这张照片就会从磁盘中读出到内核缓冲区中保存,然后操作系统将内核缓冲区中的这张图片字节数据拷贝到用户进程的缓冲区中保存下来,对应着下面这幅图:
然后用户进程会希望把缓冲区中的字节数据写到磁盘上的另外一个地方,会将数据拷贝到 Socket 缓冲区中,最终操作系统再将 Socket 缓冲区的数据写到磁盘的指定位置上。
操作系统的零拷贝
用户进程通过系统调用 read() 请求读取文件到用户空间缓冲区(第一次上下文切换),用户态 -> 核心态,数据从硬盘读取到内核空间缓冲区中(第一次数据拷贝); 系统调用返回到用户进程(第二次上下文切换),此时用户空间与内核空间共享这一块内存(缓冲区),所以不需要从内核缓冲区拷贝到用户缓冲区; 用户进程发出 write() 系统调用请求写数据到硬盘上(第三次上下文切换),此时需要将内核空间缓冲区中的数据拷贝到内核的 Socket 缓冲区中(第二次数据拷贝); 由 DMA 将 Socket 缓冲区的内容写到硬盘上(第三次数据拷贝),write() 系统调用返回(第四次上下文切换);
选择器(Selectors)
一直等待直到一个就绪的通道,再返回给用户进程 立即返回一个错误状态码给用户进程,让用户进程继续运行,不会阻塞
选择键(SelectionKey)
SelectionKey.OP_READ:套接字通道准备好进行读操作 SelectionKey.OP_WRITE:套接字通道准备好进行写操作 SelectionKey.OP_ACCEPT:服务器套接字通道接受其它通道 SelectionKey.OP_CONNECT:套接字通道准备完成连接
channel:该选择键绑定的通道 selector:轮询到该选择键的选择器 readyOps:当前就绪选择键的值 interesOps:该选择器对该通道感兴趣的所有选择键
示例:简易的客户端服务器通信。
Thread1:专门监听客户端的连接,并把通道注册到客户端选择器上; Thread2:专门监听客户端的其它 IO 状态(读状态),当客户端的 IO 状态就绪时,该选择器会轮询发现,并作相应处理。
public class NIOServer {
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
public static void main(String[] args) throws IOException {
NIOServer server = nwe NIOServer();
new Thread(() -> {
try {
// 对应IO编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(3333));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
server.acceptListener();
} catch (IOException ignored) {
}
}).start();
new Thread(() -> {
try {
server.clientListener();
} catch (IOException ignored) {
}
}).start();
}
}
// 监听客户端连接
public void acceptListener() {
while (true) {
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// (1) 每来一个新连接,注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
// 从就绪的列表中移除这个key
keyIterator.remove();
}
}
}
}
}
}
// 监听客户端的 IO 状态就绪
public void clientListener() {
while (true) {
// 批量轮询是否有哪些连接有数据可读
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 判断该通道是否读就绪状态
if (key.isReadable()) {
try {
// 获取客户端通道读入数据
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(
LocalDateTime.now().toString() + " Server 端接收到来自 Client 端的消息: " +
Charset.defaultCharset().decode(byteBuffer).toString());
} finally {
// 从就绪的列表中移除这个key
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
}
public class NIOClient {
public static final int CAPACITY = 1024;
public static void main(String[] args) throws Exception {
ByteBuffer dsts = ByteBuffer.allocate(CAPACITY);
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 3333));
socketChannel.configureBlocking(false);
Scanner sc = new Scanner(System.in);
while (true) {
String msg = sc.next();
dsts.put(msg.getBytes());
dsts.flip();
socketChannel.write(dsts);
dsts.clear();
}
}
}
总结
Java IO 体系的组成部分:BIO 和 NIO; BIO 的基本组成部分:字节流,字符流,转换流和处理流; NIO 的三大重要模块:缓冲区(Buffer),通道(Channel),选择器(Selector)以及它们的作用; NIO 与 BIO 两者的对比:同步/非同步、阻塞/非阻塞,在文件 IO 和 网络 IO 中,使用 NIO 相对于使用 BIO 有什么优势。
end
推荐阅读: